package bebetes;

import java.awt.Color;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Random;

import simu.Actionnable;
import simu.Simulateur;
import visu.Dessinable;
import visu.VisualisateurAnime;

/**
 * Classe représentant un champs de bébêtes.
 * 
 * @author COLLET Philippe
 * @author gnpt
 */
public class ChampDeBebetes extends VisualisateurAnime {
	private static final long serialVersionUID = 1L;
	/*
	 * Simulateur associé au champs, permettant de faire évoluer les bébêtes se
	 * trouvant sur le champs
	 */
	private Simulateur simu;
	// Vitesse maximun à laquelle les bébêtes peuvent aller sur le champs
	private static float vitesseMax = 10f;
	// Statut du champs
	private StatutChamp statut = StatutChamp.ARRETE;
	// Types de bébêtes que le champ contient
	private List<Class<? extends Bebete>> typesBebetes;

	/**
	 * Enumération indiquant le statut du champ de bébêtes. Il peut être
	 * démarré, arrêté ou en pause.
	 */
	public enum StatutChamp {
		/**
		 * Le champ est démarré, les bébêtes sont en train d'évoluer sur le
		 * champ.
		 */
		DEMARRE,
		/**
		 * Le champ est arrêté, les bébêtes ne bougent plus et ont été
		 * réinitialisées.
		 */
		ARRETE,
		/**
		 * Le champ est en pause, les bébêtes ne bougent plus temporairement.
		 */
		PAUSE
	};

	/**
	 * Crée un champs de taille <code>largeur</code>x<code>hauteur</code>
	 * contenant <code>nb</code> bébêtes de type {@link BebeteEmergente}.
	 * 
	 * @param largeur
	 *            largeur en pixels du champs.
	 * @param hauteur
	 *            hauteur en pixels du champs.
	 * @param nb
	 *            nombre de bébêtes que le champ contient initialement.
	 * @see #fabriqueBebetes(Class, int)
	 */
	public ChampDeBebetes(int largeur, int hauteur, int nb) {
		super(largeur, hauteur);

		this.typesBebetes = new ArrayList<Class<? extends Bebete>>();
		this.typesBebetes.add(BebeteEmergente.class);

		List<? extends Bebete> lb = this.fabriqueBebetes(BebeteEmergente.class,
				nb);

		super.setDessinables(lb);
		this.simu = new Simulateur(50, lb);
	}

	/**
	 * Crée un champs de taille <code>largeur</code>x<code>hauteur</code>
	 * contenant <code>nb</code> bébêtes dont le type passé à l'aide du
	 * paramètre <code>typesBebetes</code>.
	 * 
	 * @param largeur
	 *            largeur en pixels du champs.
	 * @param hauteur
	 *            hauteur en pixels du champs.
	 * @param typesBebetes
	 *            liste contenant les types de bébêtes que le champ doit
	 *            initialement contenir.
	 * @param nb
	 *            nombre de bébêtes que le champ contient initialement.
	 */
	public ChampDeBebetes(int largeur, int hauteur,
			List<Class<? extends Bebete>> typesBebetes, int nb) {
		super(largeur, hauteur);

		this.typesBebetes = new ArrayList<Class<? extends Bebete>>(typesBebetes);

		super.setDessinables(new ArrayList<Dessinable>());
		this.simu = new Simulateur(50, new ArrayList<Actionnable>());

		this.creerEtAjouterNombreAleatoireBebetesPourChaqueType(typesBebetes,
				nb);
	}

	/**
	 * Crée et ajoute plusieurs types de bébêtes au champ.
	 * <p>
	 * Le nombre de bébêtes créé pour chaque type de bébête est le même (on
	 * prend le nombre de bébêtes à créer <code>nb</code>, on le divise par le
	 * nombre de type de bébête et on obtient le nombre de bébêtes à créer dans
	 * le champs pour chaque type de bébêtes.
	 * <p>
	 * Si <code>nb</code> n'est pas divisible par le nombre de type de bébêtes
	 * alors le reste créer sera crée avec le type de bébête par défaut qui sera
	 * {@link BebeteEmergente}.
	 * <p>
	 * Exemple : on appelle le constructeur de la manière suivante <br>
	 * <code>
	 * List<Class<? extends Bebete>> typesBebetes = new ArrayList<Class<? extends Bebete>>(2);
	 * typesBebetes.add(BebeteType1.class);
	 * typesBebetes.add(BebeteType2.class);
	 * ChampDeBebetes cdb = new ChampDeBebetes(640, 480, typesBebetes, 15);
	 * </code>
	 * <p>
	 * On obtiendra donc un champs initialisé avec 7 bébêtes de type BebeteType1
	 * et 7 de type BebeteType2. Le reste (c'est-à-dire 1) veut dire qu'on
	 * créera une bébête de type BebeteTypeDefault qui est défini comme étant
	 * BebeteEmergente.
	 * 
	 * @param typeBebetes
	 *            liste contenant les types de bébêtes que le champ doit
	 *            initialement contenir.
	 * @param nb
	 *            nombre de bébêtes que le champ contient initialement.
	 */
	public void creerEtAjouterMemeNombreBebetesPourChaqueType(
			List<Class<? extends Bebete>> typeBebetes, int nb) {
		// On vide les listes du simulateur et du visualisateur
		super.setDessinables(new ArrayList<Dessinable>());
		this.simu.setActionnables(new ArrayList<Actionnable>());

		int nbBebetesParType = nb / typeBebetes.size();
		int nbBebeteParDefaut = nb % typeBebetes.size();

		Iterator<Class<? extends Bebete>> it = typeBebetes.iterator();
		while (it.hasNext()) {
			List<? extends Bebete> bebetes = this.fabriqueBebetes(it.next(),
					nbBebetesParType);
			this.ajouterBebetes(bebetes);
		}

		if (nbBebeteParDefaut != 0) {
			this.ajouterBebetes(this.fabriqueBebetes(BebeteEmergente.class,
					nbBebeteParDefaut));
		}
	}

	/**
	 * Crée et ajoute plusieurs types de bébêtes au champ. Contraitement à
	 * {@link #creerEtAjouterMemeNombreBebetesPourChaqueType(List, int)} qui
	 * crée le même nombre de bébêtes pour chaque type de bébête passé en
	 * paramètre, cette méthode crée un nombre aléatoire de bébêtes de chaque
	 * type en limitant bien entendu de ne pas dépasser le nombre total
	 * <code>nb</code> de bébêtes voulues sur le champ.
	 * 
	 * @param typeBebetes
	 *            liste contenant les types de bébêtes que le champ doit
	 *            initialement contenir.
	 * @param nb
	 *            nombre de bébêtes que le champ contient initialement.
	 */
	public void creerEtAjouterNombreAleatoireBebetesPourChaqueType(
			List<Class<? extends Bebete>> typeBebetes, int nb) {
		Random rand = new Random();
		int totalBebetesCrees = 0;
		int size = typeBebetes.size();

		// On vide les listes du simulateur et du visualisateur
		super.setDessinables(new ArrayList<Dessinable>());
		this.simu.setActionnables(new ArrayList<Actionnable>());

		for (int i = 0; i < size; i++) {
			int nbBebetesACreer = rand.nextInt((int) (nb / size + nb * 1.3));

			if (i != size - 1) {
				while (nbBebetesACreer + totalBebetesCrees > nb) {
					nbBebetesACreer = rand
							.nextInt((int) (nb / size + nb * 1.3));
				}

				totalBebetesCrees += nbBebetesACreer;
			} else {
				nbBebetesACreer = nb - totalBebetesCrees;
			}

			List<? extends Bebete> bebetes = this.fabriqueBebetes(typeBebetes
					.get(i), nbBebetesACreer);
			this.ajouterBebetes(bebetes);
		}
	}

	/**
	 * Ajoute une liste de bébêtes dans le champ.
	 * <p>
	 * <b>Attention</b> : il possible d'ajouter des bébêtes dans le champ
	 * uniquement ci celui-ci n'est pas démarré, c'est-à-dire si il n'est pas en
	 * train d'évoluer. Cette contrainte est posée afin d'éviter d'avoir des
	 * soucis de concurrence.
	 * 
	 * @param lb
	 *            la liste de bébêtes à ajouter dans le champs.
	 */
	@SuppressWarnings("unchecked")
	public void ajouterBebetes(List<? extends Bebete> lb) {
		if (this.statut != StatutChamp.DEMARRE) {
			List<Actionnable> la = new ArrayList<Actionnable>(lb);
			la.addAll(this.simu.getActionnables());

			List<Bebete> lp = new ArrayList<Bebete>(lb);
			lp.addAll((ArrayList<? extends Bebete>) super.getPositionnables());

			this.simu.setActionnables(la);
			super.setDessinables(lp);
		} else {
			throw new IllegalStateException(
					"Impossible d'ajouter des bébêtes quand le champs est en train d'évoluer.");
		}
	}

	/**
	 * Arrête le champs : stoppe l'affichage graphique associé au champs et
	 * ensuite le simulateur faisant évoluer les bébêtes.
	 * 
	 * @see simu.Simulateur#arrete()
	 */
	@Override
	public void arrete() {
		if (this.statut == StatutChamp.DEMARRE
				|| this.statut == StatutChamp.PAUSE) {
			super.arrete();
			this.simu.arrete();
			this.statut = StatutChamp.ARRETE;

			this.creerEtAjouterNombreAleatoireBebetesPourChaqueType(
					this.typesBebetes, this.getNombreDeBebetes());

			super.repaint();
		}
	}

	/**
	 * Met en pause le champs de bébêtes.
	 */
	public void pause() {
		if (this.statut == StatutChamp.DEMARRE) {
			super.arrete();
			this.simu.arrete();
			this.statut = StatutChamp.PAUSE;
		} else if (this.statut == StatutChamp.PAUSE) {
			this.demarre();
			this.statut = StatutChamp.DEMARRE;
		}
	}

	/**
	 * Fait évoluer le champs d'un pas.
	 */
	public void unPas() {
		if (this.statut == StatutChamp.ARRETE) {
			this.demarre();
			this.pause();
		}

		if (this.statut == StatutChamp.DEMARRE) {
			this.pause();
		}

		if (this.statut == StatutChamp.PAUSE) {
			this.simu.agiterLesActionnables();
			super.repaint();
		}
	}

	/**
	 * Démarre le champs : lance le simulateur afin de faire évoluer les bébêtes
	 * et lance l'afficheur permettant d'avoir une représentation graphique à
	 * l'écran.
	 * 
	 * @see simu.Simulateur#demarre()
	 */
	@Override
	public void demarre() {
		if (this.statut == StatutChamp.ARRETE
				|| this.statut == StatutChamp.PAUSE) {
			this.simu.demarre();
			super.demarre();
			this.statut = StatutChamp.DEMARRE;
		}
	}

	/**
	 * Fabrique de bébêtes.
	 * 
	 * Chaque bébête créée à une couleur unique.
	 * 
	 * @param typeBebete
	 *            liste contenant les types de bébêtes à créer.
	 * 
	 * @param nb
	 *            le nombre de bébêtes à créer.
	 * @return une liste contenant les bébêtes qui ont été créées.
	 */
	public List<? extends Bebete> fabriqueBebetes(
			Class<? extends Bebete> typeBebete, int nb) {
		ArrayList<Bebete> nouvBebetes = new ArrayList<Bebete>();
		Random generateur = new Random();

		double racineCubiqueDuNombreDeBebetes = Math
				.pow((double) nb, 1.0 / 3.0);
		float etapeDeCouleur = (float) (1.0 / racineCubiqueDuNombreDeBebetes);
		float r = 0.0f;
		float g = 0.0f;
		float b = 0.0f;

		for (int i = 0; i < nb; i++) {
			int x = (int) (generateur.nextFloat() * this.largeur);
			if (x > this.largeur - Bebete.TAILLEGRAPHIQUE)
				x -= Bebete.TAILLEGRAPHIQUE;
			int y = (int) (generateur.nextFloat() * this.hauteur);
			if (y > this.hauteur - Bebete.TAILLEGRAPHIQUE)
				y -= Bebete.TAILLEGRAPHIQUE;
			float direction = (float) (generateur.nextFloat() * 2 * Math.PI);
			float vitesse = generateur.nextFloat() * ChampDeBebetes.vitesseMax;
			r += etapeDeCouleur;

			if (r > 1.0) {
				r -= 1.0f;
				g += etapeDeCouleur;
				if (g > 1.0) {
					g -= 1.0f;
					b += etapeDeCouleur;
					if (b > 1.0)
						b -= 1.0f;
				}
			}

			/*
			 * On trouve le constructeur à utiliser (ChampsDeBebetes, int, int,
			 * float, float, Color) du typeBebete par introspection
			 */
			Constructor<? extends Bebete> constructeur = null;
			try {
				constructeur = typeBebete.getConstructor(new Class[] {
						ChampDeBebetes.class, int.class, int.class,
						float.class, float.class, Color.class });
			} catch (SecurityException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			} catch (NoSuchMethodException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}

			/*
			 * On ajoute une nouvelle bébête construite via newInstance dans la
			 * liste des bébêtes créées
			 */
			try {
				nouvBebetes.add((Bebete) constructeur.newInstance(new Object[] {
						this, x, y, direction, vitesse, new Color(r, g, b) }));
			} catch (IllegalArgumentException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			} catch (InstantiationException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			} catch (IllegalAccessException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			} catch (InvocationTargetException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}
		return nouvBebetes;
	}

	/**
	 * Accesseur pour le délai d'attente avant chaque rafraîchissement de la
	 * simulation.
	 * 
	 * @return le délai d'attente avant chaque rafraîchissement de la
	 *         simulation.
	 */
	public int getDelaiSimulation() {
		return this.simu.getDelaiSimulation();
	}

	/**
	 * Accesseur pour connaître le nombre de bébêtes actuellement sur le champs.
	 * 
	 * @return le nombre de bébêtes actuellement sur le champs.
	 */
	public int getNombreDeBebetes() {
		return this.simu.getActionnables().size();
	}

	/**
	 * Accesseur pour connaître le statut du champ.
	 * 
	 * @return le statut du champ.
	 */
	public StatutChamp getStatutChamp() {
		return this.statut;
	}

	/**
	 * Accesseur pour connaître les types de bébêtes pouvant être créés sur le
	 * champ.
	 * 
	 * @return une liste des types de bébêtes pouvant être créés sur le champ.
	 */
	public List<Class<? extends Bebete>> getTypesBebetes() {
		return this.typesBebetes;
	}

	/**
	 * @param delaiSimu
	 *            le nouveau délai d'attente avant chaque rafraîchissement de la
	 *            simulation.
	 * @see simu.Simulateur#setDelaiSimulation(int)
	 */
	public void setDelaiSimulation(int delaiSimu) {
		this.simu.setDelaiSimulation(delaiSimu);
	}

	/**
	 * Mutateur pour les bébêtes dessinables.
	 * 
	 * @param bebetes
	 *            les nouvelles bébêtes dessinables.
	 */
	public void setBebetes(List<? extends Bebete> bebetes) {
		super.setDessinables(bebetes);
	}

	/**
	 * Mutateur pour la liste des types de bébête.
	 * 
	 * @param typesBebetes
	 *            la nouvelle liste des types de bébête pouvant se trouver sur
	 *            le champ.
	 */
	public void setTypeBebetes(List<Class<? extends Bebete>> typesBebetes) {
		this.typesBebetes = typesBebetes;
	}

	/**
	 * Permet de rafraîchir le visualisateur (la vue du champ).
	 */
	public void rafraichirVisualisateur() {
		super.repaint();
	}
}
